﻿using System;
using System.Collections.Generic;
using System.Data.Services.Client;
using System.Linq;
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.StorageClient;

namespace jp110311.StorageModels
{
    [CLSCompliant(false)]
    public static class BbsEntityServices
    {
        internal const string BbsEntityTableName = "bbsentities";
        public static bool UseDevelopmentStorage;
        private static bool _tableCreated;

        static readonly TableStoragePredicateUtility<BbsEntity> PredicateUtility = new TableStoragePredicateUtility<BbsEntity>();

        public static IEnumerable<BbsEntity> GetBbsEntities( string zipCode, int skipCount, int maxCount )
        {
            TableServiceContext context = GetDataServiceContext();
            BbsIndexEntity index = GetIndex(context, zipCode);
            if (index == null) return EmptyEntitySequence;

            string[] msgIdArray =
                (index.MessageIDs0 != null
                     ? index.MessageIDs0.Split(IndexItemSeparator, StringSplitOptions.RemoveEmptyEntries)
                     : Enumerable.Empty<string>())
                    .Concat(index.MessageIDs1 != null
                                ? index.MessageIDs1.Split(IndexItemSeparator, StringSplitOptions.RemoveEmptyEntries)
                                : Enumerable.Empty<string>())
                    .Concat(index.MessageIDs2 != null
                                ? index.MessageIDs2.Split(IndexItemSeparator, StringSplitOptions.RemoveEmptyEntries)
                                : Enumerable.Empty<string>())
                    .Concat(index.MessageIDs3 != null
                                ? index.MessageIDs3.Split(IndexItemSeparator, StringSplitOptions.RemoveEmptyEntries)
                                : Enumerable.Empty<string>())
                    .ToArray();

            var msgIds = msgIdArray.Reverse().Skip(skipCount).Take(maxCount);
            var lambda = PredicateUtility.BuildLambda(zipCode, msgIds);
            if (lambda == null) return EmptyEntitySequence;
            var q = context.CreateQuery<BbsEntity>(BbsEntityTableName);
            return q.Where(lambda).AsEnumerable().ToArray();
        }

        private static BbsIndexEntity GetIndex(TableServiceContext context, string zipCode)
        {
            var qidx = context.CreateQuery<BbsIndexEntity>(BbsEntityTableName);
            return qidx.Where(i => i.PartitionKey == zipCode && i.RowKey == "INDEX")
                .SingleOrDefault();
        }

        private static readonly TableStoragePredicateUtility<BbsTagEntity> TagPredUtil = new TableStoragePredicateUtility<BbsTagEntity>();

        public static IEnumerable<BbsTagEntity> GetBbsTags( string zipCode, int skipCount, int maxCount )
        {
            TableServiceContext context = GetDataServiceContext();
            var index = GetIndex(context, zipCode);
            if (index == null) return EmptyTagEntities;
            IEnumerable<string > tagItems = index.TagList == null
                                    ? Enumerable.Empty<string>()
                                    : index.TagList.Split(IndexItemSeparator, StringSplitOptions.RemoveEmptyEntries);
            tagItems = tagItems.Reverse().ToArray();
            tagItems = tagItems.Skip(skipCount).Take(maxCount);
            var q = context.CreateQuery<BbsTagEntity>(BbsEntityTableName);
            var lambda = TagPredUtil.BuildLambda(zipCode, tagItems.Select(tag => "T" + tag));
            if (lambda == null) return EmptyTagEntities;
            return q.Where(lambda).AsEnumerable();
        }

        public static int GetBbsEntitiesCount( string zipCode )
        {
            TableServiceContext context = GetDataServiceContext();
            var indexEntity = GetIndex(context, zipCode);
            if (indexEntity == null) return 0;
            return indexEntity.MessageCount;
        }

        public static IEnumerable<BbsEntity> GetBbsEntityByTag( string zipCode, string tag, int skipCount,int maxCount )
        {
            TableServiceContext context = GetDataServiceContext();
            var tagEntity = GetTagEntity(context, zipCode, tag);
            if (tagEntity == null) return EmptyEntitySequence;
            if (string.IsNullOrEmpty(tagEntity.BbsEntityRowKeys))
            {
                return EmptyEntitySequence;
            }
            string[] rowKeys = tagEntity.BbsEntityRowKeys.Split(';');
            var fetchItems = rowKeys.Skip(skipCount).Take(maxCount);
            var lambda = PredicateUtility.BuildLambda(zipCode, fetchItems);
            if (lambda == null) return EmptyEntitySequence;
            var q = context.CreateQuery<BbsEntity>(BbsEntityTableName);
            return q.Where(lambda).AsEnumerable();
        }

        private static BbsTagEntity GetTagEntity(TableServiceContext context, string zipCode, string tag)
        {
            var qtag = context.CreateQuery<BbsTagEntity>(BbsEntityTableName);
            string tagRowKey = "T" + tag;
            return qtag.Where(te => te.PartitionKey == zipCode && te.RowKey == tagRowKey).SingleOrDefault();
        }

        private static int _localcount = int.MaxValue;

        public static string AddBbsMessage( string zipCode,DateTime postAt, string title,string content, string authorZipCode, string authorNick )
        {
            string resultMessageId = null;
            TransactionRetryLoop(
                () =>
                {
                    var lcnt = System.Threading.Interlocked.Decrement(ref _localcount);

                    var bbsEntity = new BbsEntity(zipCode, postAt, title, content, authorZipCode, authorNick, lcnt);
                    TableServiceContext context = GetDataServiceContext();
                    context.AddObject(BbsEntityTableName,
                                      bbsEntity);
                    var indexEntity = GetIndex(context, zipCode);
                    if (indexEntity == null)
                    {
                        var bbsIndexEntity = new BbsIndexEntity(zipCode)
                                             {
                                                 MessageCount = 1
                                             };
                        AddMessageIdToIndex(bbsIndexEntity, bbsEntity.RowKey);
                        context.AddObject(BbsEntityTableName, bbsIndexEntity);
                    }
                    else
                    {
                        indexEntity.MessageCount++;
                        AddMessageIdToIndex(indexEntity, bbsEntity.RowKey);
                        context.UpdateObject(indexEntity);
                    }

                    resultMessageId = bbsEntity.RowKey;
                    DoSaveChanges(context);
                }
            );
            return resultMessageId;
        }

        private static void AddMessageIdToIndex(BbsIndexEntity bbsIndexEntity, string msgId)
        {
            if (bbsIndexEntity.MessageIDs0 == null)
            {
                bbsIndexEntity.MessageIDs0 = msgId;
                return;
            }
            if (bbsIndexEntity.MessageIDs0.Length < 16000)
            {
                bbsIndexEntity.MessageIDs0 += ";" + msgId;
                return;
            }
            if (bbsIndexEntity.MessageIDs1 == null)
            {
                bbsIndexEntity.MessageIDs1 = ";"+ msgId;
                return;
            }
            if (bbsIndexEntity.MessageIDs1.Length < 16000)
            {
                bbsIndexEntity.MessageIDs1 += ";" + msgId;
                return;
            }
            if (bbsIndexEntity.MessageIDs2 == null)
            {
                bbsIndexEntity.MessageIDs2 = ";" + msgId;
                return;
            }
            if (bbsIndexEntity.MessageIDs2.Length < 16000)
            {
                bbsIndexEntity.MessageIDs2 += ";" + msgId;
                return;
            }
            if (bbsIndexEntity.MessageIDs3 == null)
            {
                bbsIndexEntity.MessageIDs3 = ";" + msgId;
            }
            if (bbsIndexEntity.MessageIDs3.Length < 16000)
            {
                bbsIndexEntity.MessageIDs3 += ";" + msgId;
                return;
            }
            // 古いの自動忘却！ ページングで見れなくなるだけで、Tag経由での参照は生きてるはず。
            bbsIndexEntity.MessageIDs0 = bbsIndexEntity.MessageIDs1;
            bbsIndexEntity.MessageIDs1 = bbsIndexEntity.MessageIDs2;
            bbsIndexEntity.MessageIDs2 = bbsIndexEntity.MessageIDs3;
            bbsIndexEntity.MessageIDs3 = ";" + msgId;
        }

        public static string AddBbsMessage(string zipCode, DateTime postAt, string title, string content, string authorZipCode, string authorNick, string tag)
        {
            string msgId = AddBbsMessage(zipCode, postAt, title, content, authorZipCode, authorNick);
            if (!string.IsNullOrWhiteSpace(tag))
            {
                AddTagToBbsEntity(zipCode, msgId, tag);
            }
            return msgId;
        }


        private static void TransactionRetryLoop(Action action)
        {
            int retryCount = 0;
            bool success = false;
            do
            {
                try
                {
                    action();
                    success = true;
                }
                catch (DataServiceRequestException)
                {
                    retryCount++;
                    if (retryCount > 3) throw;
                }
            } while (!success);
        }

        public static event EventHandler BeforeSaveChanges;

        private static void DoSaveChanges(TableServiceContext context)
        {
            if (BeforeSaveChanges != null) BeforeSaveChanges(null, EventArgs.Empty);

            if( UseDevelopmentStorage ) context.SaveChanges();
            context.SaveChanges(SaveChangesOptions.Batch);
        }

        public static void AddTagToBbsEntity( string zipCode, string bbsMsgId, string tag )
        {
            TransactionRetryLoop(
                () =>
                {
                    TableServiceContext context = GetDataServiceContext();
                    var q = context.CreateQuery<BbsEntity>(BbsEntityTableName);
                    var indexEntity = GetIndex(context, zipCode);
                    if (indexEntity == null)
                    {
                        indexEntity = new BbsIndexEntity( zipCode );
                        indexEntity.TagList += ";" + tag;
                        context.AddObject(BbsEntityTableName,indexEntity);
                    }
                    else
                    {
                        bool needUpdate = false;
                        if (indexEntity.TagList != null)
                        {
                            var readyTags = indexEntity.TagList.Split(IndexItemSeparator,
                                                                      StringSplitOptions.RemoveEmptyEntries);
                            if (!readyTags.Contains(tag))
                            {
                                indexEntity.TagList += ";" + tag;
                                needUpdate = true;
                            }
                        }
                        else
                        {
                            indexEntity.TagList = tag;
                            needUpdate = true;
                        }
                        if( needUpdate) context.UpdateObject(indexEntity);
                    }

                    var msg = q.Where(m => m.PartitionKey == zipCode && m.RowKey == bbsMsgId).Single();
                    string tagRowKey = "T" + tag;

                    var tagEntity = GetTagEntity(context, zipCode, tag);
                    if (tagEntity == null)
                    {
                        tagEntity = new BbsTagEntity(zipCode, tagRowKey, bbsMsgId)
                                    {
                                        MessageCount = 1
                                    };
                        context.AddObject(BbsEntityTableName, tagEntity);
                        AddTagToMessageEntity(context, msg, tag);
                    }
                    else
                    {
                        var listMsg =
                            tagEntity.BbsEntityRowKeys.Split(IndexItemSeparator, StringSplitOptions.RemoveEmptyEntries).ToList();
                        listMsg.Add(bbsMsgId);
                        listMsg.Sort();
                        tagEntity.BbsEntityRowKeys = string.Join(";", listMsg);
                        tagEntity.MessageCount++;
                        context.UpdateObject(tagEntity);
                        AddTagToMessageEntity(context, msg, tag);
                    }
                    DoSaveChanges(context);
                }
                );
        }

        public static int GetBbsTagedEntitiesCount(string zipCode,string tag)
        {
            TableServiceContext context = GetDataServiceContext();
            var tagEntity = GetTagEntity(context, zipCode, tag);
            if (tagEntity == null) return 0;
            return tagEntity.MessageCount;
        }

        public static void RemoveTagFromBbsEntity( string zipCode, string bbsMsgId, string tag )
        {
            TransactionRetryLoop(
                () =>
                {
                    TableServiceContext context = GetDataServiceContext();
                    var q = context.CreateQuery<BbsEntity>(BbsEntityTableName);

                    var msg = q.Where(m => m.PartitionKey == zipCode && m.RowKey == bbsMsgId).Single();
                    var tagEntity = GetTagEntity(context, zipCode, tag);
                    if (tagEntity == null)
                    {
                        return; // 削除するべきタグがない
                    }
                    msg.TagList = string.Join(";", msg.TagList.Split(';').Where(t => t != tag));
                    tagEntity.BbsEntityRowKeys = string.Join(";",
                                                             tagEntity.BbsEntityRowKeys.Split(';').Where(
                                                                 k => k != bbsMsgId));
                    tagEntity.MessageCount--;
                    context.UpdateObject(msg);
                    context.UpdateObject(tagEntity);
                    DoSaveChanges(context);
                }
                );
        }

        private static void AddTagToMessageEntity(TableServiceContext context, BbsEntity msg, string tag)
        {
            string tagList = msg.TagList;

            List<string> listTags =
                tagList == null
                    ? new List<string>()
                    : tagList.Split(IndexItemSeparator, StringSplitOptions.RemoveEmptyEntries).ToList();
            listTags.Add(tag);
            listTags.Sort();
            msg.TagList = string.Join(";", listTags);
            context.UpdateObject(msg);
        }

        private static TableServiceContext GetDataServiceContext()
        {
            CloudStorageAccount account = GetAccount();
            CloudTableClient tableClient = account.CreateCloudTableClient();
            if (!_tableCreated)
            {
                tableClient.CreateTableIfNotExist(BbsEntityTableName);
                _tableCreated = true;
            }
            TableServiceContext tableServiceContext = tableClient.GetDataServiceContext();
            tableServiceContext.IgnoreResourceNotFoundException = true;
            tableServiceContext.RetryPolicy = RetryPolicies.Retry(3, TimeSpan.FromMilliseconds(20));
            if (DataServiceContextCreated != null)
            {
                DataServiceContextCreated(null, new DataServiceContextCreatedEventArgs(tableServiceContext));
            }
            return tableServiceContext;
        }

        public static event EventHandler<DataServiceContextCreatedEventArgs> DataServiceContextCreated;
        public static Func<CloudStorageAccount> GetAccountDelegate;
        private static readonly char[] IndexItemSeparator = new[] {';'};
        private static readonly IEnumerable<BbsEntity> EmptyEntitySequence = Enumerable.Empty<BbsEntity>();
        private static readonly IEnumerable<BbsTagEntity> EmptyTagEntities = Enumerable.Empty<BbsTagEntity>();

        private static CloudStorageAccount GetAccount()
        {
            if (UseDevelopmentStorage) return CloudStorageAccount.DevelopmentStorageAccount;
            if (GetAccountDelegate != null) return GetAccountDelegate();
            return CloudStorageAccount.FromConfigurationSetting("StorageAccountConnection");
        }
    }
}
